package triStorage;

import java.util.concurrent.locks.*;
import java.util.concurrent.*;
import transaction.*;

import utils.*;

//Not Efficient ?
public class TriMMPFile {

	public long PAGE_SIZE;
	
	private int maxPageId;
	
	private Page[] buffer;
	private boolean[] isDirty;
	private boolean[] LRU;
	private int[] pinCnt;
	
	private TriDiskFile diskFile;
	
	private ConcurrentHashMap<Integer, Integer> map;
	private ReentrantLock indexLock;
	
	private byte[] zeros;
	
	private Object maxPageIdLock = new Object();
	
	private static int CLOSE_WAIT_TIME = 1000;
	
	public final String FileName;
	
	public TriMMPFile(TriDiskFile file, int length, int pageSize) {
		FileName = file.FileName;
		
		this.diskFile = file;
		this.PAGE_SIZE = pageSize;
		
		buffer = new Page[length];
		isDirty = new boolean[length];
		LRU = new boolean[length];
		pinCnt = new int[length];
		
		map = new ConcurrentHashMap<Integer, Integer>();
		indexLock = new ReentrantLock();
		
		zeros = new byte[pageSize];
		Converter.Int2Bytes(0, zeros, 0);
	}
	
	public TriMMPFile(String filename, int length, int pageSize) {
		FileName = filename;
		
		this.diskFile = new TriDiskFile(filename);
		this.PAGE_SIZE = pageSize;
		
		buffer = new Page[length];
		isDirty = new boolean[length];
		LRU = new boolean[length];
		pinCnt = new int[length];
		
		map = new ConcurrentHashMap<Integer, Integer>();
		indexLock = new ReentrantLock();
		
		zeros = new byte[pageSize];
		Converter.Int2Bytes(0, zeros, 0);
	}
	public boolean open() {
		boolean isNew = diskFile.open();
		if (isNew) {
			maxPageId = 0;
			//allocNewPage();
		}
		else 
			maxPageId = (int)((diskFile.getFileLength() + PAGE_SIZE - 1)/ PAGE_SIZE);
		return isNew;
	}
	
	public void close() {
		try {
			while (!indexLock.tryLock()) {
				Thread.sleep(TriMMPFile.CLOSE_WAIT_TIME);
			}
		} 
		catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		finally {
			if (indexLock.isHeldByCurrentThread()) {
				for (int i = 0; i < buffer.length; ++i) 
					flushPage(i);
				indexLock.unlock();
			}
			else
				System.err.println("Close diskFile without the lock!");
			diskFile.close();
		}
	}
	
	public int getmaxPageId() {
		synchronized (maxPageIdLock) {
			return maxPageId - 1;
		}
	}
	
	public int allocNewPage() {
		synchronized (maxPageIdLock) {
			long offset = maxPageId * PAGE_SIZE;
			diskFile.write(offset, zeros, 0, (int)PAGE_SIZE);
			return maxPageId++;
		}
	}
	
	public int appendInt(int pageId, int offset, int value, Transaction tx) {
		int index = pinPage(pageId);
		int ret = buffer[index].writeInt(offset, value);
		if (ret != Constants.W_ERROCODE) {		
			tx.appendInt(this, pageId, offset, value);
			isDirty[index] = true;
		}
		
		unpinPage(index);
		
		//System.out.println("write " + value + " on page = " + pageId + " with pos = " + offset);
		return ret;
	}
	
	public int writeInt(int pageId, int offset, int value, Transaction tx) {
		int index = pinPage(pageId);
		
		int o = buffer[index].readInt(offset);
		int ret = buffer[index].writeInt(offset, value);
		if (ret != Constants.W_ERROCODE) {
			//tx.appendInt(this, pageId, offset, value);
			tx.writeInt(this, pageId, offset, o, value);
			isDirty[index] = true;
		}
		
		unpinPage(index);
		
		//System.out.println("write " + value + " on page = " + pageId + " with pos = " + offset);
		return ret;
	}
	
	public int appendLong(int pageId, int offset, long value, Transaction tx) {
		int index = pinPage(pageId);
		int ret = buffer[index].writeLong(offset, value);
		if (ret != Constants.W_ERROCODE) {
			tx.appendLong(this, pageId, offset, value);
			isDirty[index] = true;
		}
		
		unpinPage(index);
		
		//System.out.println("write " + value + " on page = " + pageId + " with pos = " + offset);
		return ret;
	}
	
	public int writeLong(int pageId, int offset, long value, Transaction tx) {
		int index = pinPage(pageId);
		
		long o = buffer[index].readInt(offset);
		int ret = buffer[index].writeLong(offset, value);
		if (ret != Constants.W_ERROCODE) {
			tx.writeLong(this, pageId, offset, o, value);
			isDirty[index] = true;
		}
		
		unpinPage(index);
		
		//System.out.println("write " + value + " on page = " + pageId + " with pos = " + offset);
		return ret;
	}
	
	public int appendBytes(int pageId, int offset, byte[] value, int start, int length,
			Transaction tx) {
		
		int index = pinPage(pageId);
		int ret = buffer[index].writeBytes(offset, value, start, length);
		
		if (ret > 0) {
			tx.appendBytes(this, pageId, offset, value, start, ret);
			isDirty[index] = true;
		}
		
		unpinPage(index);
		
		//System.out.println("write string on page = " + pageId + " with pos = " + offset);
		return ret;
	}
	
	public Integer readInt(int pageId, int offset) {
		int index = pinPage(pageId);
		Integer value = buffer[index].readInt(offset);
		//System.out.println("read " + value + " on page = " + pageId + " with pos = " + offset);
		unpinPage(index);
		return value;
	}
	
	public Long readLong(int pageId, int offset) {
		int index = pinPage(pageId);
		Long value = buffer[index].readLong(offset);
		//System.out.println("read " + value + " on page = " + pageId + " with pos = " + offset);
		unpinPage(index);
		return value;
	}

	public int readBytes(int pageId, int offset, byte[] value, int start, int length) {
		int index = pinPage(pageId);
		int ret = buffer[index].readBytes(offset, value, start, length);
		unpinPage(index);
		
		//System.out.println("read string on page = " + pageId + " with pos = " + offset);
		return ret;
	}
	
	//return index
	private int pinPage(int pageId) {
		Integer index = map.get(pageId);
		
		if (index != null) {
			synchronized(buffer[index]) {
				if (buffer[index].pageId == pageId) {
					++pinCnt[index];
					LRU[index] = true;
					return index;
				}
			}
		}
		
		return loadPageInBuffer(pageId);
	}
	
	private int cursor = -1;
	
	private int loadPageInBuffer(int pageId) {
		indexLock.lock();
		try {
			Integer index = map.get(pageId);
			if (index != null) {
				synchronized(buffer[index]) {
					++pinCnt[index];
					LRU[index] = true;
					return index;
				}
			}
			
			//spinning the cursor
			while (true) {
				cursor = (cursor + 1) % buffer.length;
				
				if (buffer[cursor] == null) {
					//create page and pin
					buffer[cursor] = new Page((int)PAGE_SIZE);
					
					loadPage(cursor, pageId);
					pinCnt[cursor] = 1;
					isDirty[cursor] = false;
					LRU[cursor] = true;
					map.put(pageId, cursor);
					
					//System.out.println("swap in " + pageId);
					return cursor;
				}
				
				synchronized(buffer[cursor]) {
					if (pinCnt[cursor] == 0 && !LRU[cursor]) {
						//replace and pin
						//System.out.println("swap out " + buffer[cursor].pageId + " with in " + pageId);
						
						map.remove(buffer[cursor].pageId);
						loadPage(cursor, pageId);
						pinCnt[cursor] = 1;
						isDirty[cursor] = false;
						LRU[cursor] = true;
						map.put(pageId, cursor);
						
						return cursor;
					}
					else if (pinCnt[cursor] == 0 && LRU[cursor]) {
						LRU[cursor] = false;
					}
				}
				
			}
		}
		finally {
			indexLock.unlock();	
		}
	}
	
	private void unpinPage(int index) {
		synchronized (buffer[index]) {
			--pinCnt[index];
		}
	}
	
	private void loadPage(int index, int pageId) {
		flushPage(index);
		buffer[index].pageId = pageId;
		diskFile.read(PAGE_SIZE * pageId, buffer[index].bytes, 0, (int)PAGE_SIZE);
	}
	
	private void flushPage(int index) {
		if (isDirty[index])
			diskFile.write(PAGE_SIZE * buffer[index].pageId, buffer[index].bytes, 0, (int)PAGE_SIZE);
	}
		
}
